﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace AdamDsk
{
    public class DskFile
    {
        protected readonly byte[] data;
        protected readonly uint length;

        public DskFile(uint length)
        {
            data = new byte[length];
            this.length = length;
            this.Directory = new DirectoryBlock(new byte[1024]);
            Directory.Entries[0] = new DirectoryEntry("BOOT\x3", DirectoryAttributes.PermanentlyProtected, 0, 1, 1, 0, DateTime.Today);
            Directory.Entries[1] = new DirectoryEntry("DIRECTORY\x3", DirectoryAttributes.PermanentlyProtected, 1, 1, 1, 0, DateTime.Today);
            Directory.Entries[2] = new DirectoryEntry("BLOCKS LEFT\x3", DirectoryAttributes.NotAFile, 2, (ushort)(length / 1024 - 2), (ushort)(length / 1024 - 2), (ushort)(length / 1024 - 2), DateTime.Today);
        }

        public uint TotalBlocks => length / 1024;

        public DskFile LoadFile(Stream file)
        {
            DskFile result = null;

            if (file.Length == length)
            {
                file.Read(data, 0, (int)length);

                result = this;
            }
            else
            {
                throw new Exception("File stream and disk image are not the same size.");
            }

            var bytes = new byte[1024];
            Array.ConstrainedCopy(data, 1024, bytes, 0, 1024);

            Directory = new DirectoryBlock(bytes);

            Debug.WriteLine($"Loaded image {Directory.Header.Name} with {BitConverter.ToUInt32(Directory.Header.VOL_SIZE, 0)} blocks of data.");

            return result;
        }

        public DskFile SaveFile(Stream file)
        {
            if (file == Stream.Null) throw new ArgumentNullException(nameof(file), "file cannot be null.");

            file.Write(data, 0, 1024);

            file.Flush();

            file.Write(Directory.ToArray(), 0, 1024);

            file.Flush();

            file.Write(data, 2048, (int)(this.length - 2048));

            file.Flush();

            return this;
        }

        public DirectoryEntry DeleteFile(Stream file, DirectoryEntry entry)
        {
            if(entry == null) throw new ArgumentNullException(nameof(entry));
            if (file == Stream.Null) throw new ArgumentNullException(nameof(file));

            //if ((entry.DIR_ATTRIBUTES & DirectoryAttributes.FileDeleted) == DirectoryAttributes.FileDeleted)
            //{

            //}
            //else
            //{
            //    entry.DIR_ATTRIBUTES |= DirectoryAttributes.FileDeleted;
            //}

            entry.DIR_ATTRIBUTES ^= DirectoryAttributes.FileDeleted;

            SaveFile(file);

            return entry;
        }

        public DirectoryEntry AddFile(Stream fileData, string filename, DirectoryAttributes attributes)
        {
            if (filename.Length > 12) filename = filename.Substring(0, 10) + filename.Substring(filename.Length - 1) + "\x3";

            var startBlock = 2u;
            var index = 38;
            DirectoryEntry entry = null;

            entry = Directory.Entries[--index];
            while (entry != null && entry.Name != "BLOCKS LEFT" && entry.Name != "DIRECTORY")
            {
                entry = Directory.Entries[--index];
            }

            if (entry.Name == "BLOCKS LEFT")
            {
                --index;

                entry = Directory.Entries[index];

                if (entry.Name != "DIRECTORY") ++index;
            }

            startBlock = BitConverter.ToUInt32(entry.DIR_START_BLOCK, 0) +
                             BitConverter.ToUInt16(entry.DIR_MAX_LENGTH, 0);

            if (entry.Name == "DIRECTORY") index++;

            if (index < 36)
            {
                var filelength = (ushort)(fileData.Length / 1024 + (fileData.Length % 1024 > 0 ? 1 : 0));
                Directory.Entries[index] = new DirectoryEntry(filename, attributes,
                    startBlock, filelength,
                    (ushort)(fileData.Length / 1024 + (fileData.Length % 1024 > 0 ? 1 : 0)),
                    (ushort)(fileData.Length % 1024), DateTime.Now);

                Directory.Entries[index + 1] = new DirectoryEntry("BLOCKS LEFT\x3", DirectoryAttributes.NotAFile,
                    startBlock + filelength, (ushort)(length / 1024 - (startBlock + filelength)),
                    (ushort)(length / 1024 - (startBlock + filelength)),
                    (ushort)(length / 1024 - (startBlock + filelength)), DateTime.Now);

                var filedata = new byte[fileData.Length];

                fileData.Read(filedata, 0, filedata.Length);

                Array.ConstrainedCopy(filedata, 0, data, (int)startBlock*1024, filedata.Length);

                return Directory.Entries[index];
            }

            return null;
        }

        public DirectoryEntry GetFile(string filename, Stream file)
        {
            var entry = Directory.Find(filename);

            if (entry != null)
            {
                var start = BitConverter.ToUInt16(entry.DIR_START_BLOCK, 0) * 1024;
                var used = BitConverter.ToUInt16(entry.DIR_USED_LENGTH, 0) - 1;
                var l = used * 1024 + BitConverter.ToUInt16(entry.DIR_LAST_COUNT, 0);
                var result = new byte[l];
                Array.ConstrainedCopy(data, start, result, 0, l);

                file.Write(result, 0, result.Length);

                file.Flush();
            }

            return entry;
        }

        public byte[] GetBlock(uint blockNumber)
        {
            var result = new byte[1024];

            if (blockNumber * 1024 > length) throw new ArgumentException("Block Number does not exist.");

            for (uint i = 0; i < 1024; ++i)
            {
                result[i] = data[i + blockNumber * 1024u];
            }

            return result;
        }

        public DirectoryBlock Directory { get; protected set; }
    }

    public class DirectoryEntry
    {
        public byte[] DIR_NAME { get; } = new byte[12];
        public DirectoryAttributes DIR_ATTRIBUTES { get; set; } = DirectoryAttributes.NotAFile;
        public byte[] DIR_START_BLOCK { get; } = new byte[4];
        public byte[] DIR_MAX_LENGTH { get; } = new byte[2];
        public byte[] DIR_USED_LENGTH { get; } = new byte[2];
        public byte[] DIR_LAST_COUNT { get; } = new byte[2];
        public byte[] CREATION_DATE { get; } = new byte[3];

        public DirectoryEntry(string name, DirectoryAttributes attributes, uint startBlock, ushort maxLength,
            ushort usedLength, ushort lastCount, DateTime creationDate)
        {
            Rename(name);

            DIR_ATTRIBUTES = attributes;

            DIR_START_BLOCK[0] = BitConverter.GetBytes(startBlock)[0];
            DIR_START_BLOCK[1] = BitConverter.GetBytes(startBlock)[1];
            DIR_START_BLOCK[2] = BitConverter.GetBytes(startBlock)[2];
            DIR_START_BLOCK[3] = BitConverter.GetBytes(startBlock)[3];

            DIR_MAX_LENGTH[0] = BitConverter.GetBytes(maxLength)[0];
            DIR_MAX_LENGTH[1] = BitConverter.GetBytes(maxLength)[1];

            DIR_USED_LENGTH[0] = BitConverter.GetBytes(usedLength)[0];
            DIR_USED_LENGTH[1] = BitConverter.GetBytes(usedLength)[1];

            DIR_LAST_COUNT[0] = BitConverter.GetBytes(lastCount)[0];
            DIR_LAST_COUNT[1] = BitConverter.GetBytes(lastCount)[1];

            CREATION_DATE[0] = (byte)(creationDate.Year - 1900);
            CREATION_DATE[1] = (byte)(creationDate.Month - 1);
            CREATION_DATE[2] = (byte)(creationDate.Day - 1);
        }

        public DirectoryEntry(byte[] bytes)
        {
            if (bytes.Length != 26) throw new ArgumentException("Invalid directory entry data block.");

            var pointer = 0;
            for (int i = 0; i < 12; ++i)
            {
                DIR_NAME[i] = bytes[pointer++];
            }

            DIR_ATTRIBUTES = (DirectoryAttributes)bytes[pointer++];

            for (int i = 0; i < 4; ++i)
            {
                DIR_START_BLOCK[i] = bytes[pointer++];
            }

            for (int i = 0; i < 2; ++i)
            {
                DIR_MAX_LENGTH[i] = bytes[pointer++];
            }

            for (int i = 0; i < 2; ++i)
            {
                DIR_USED_LENGTH[i] = bytes[pointer++];
            }

            for (int i = 0; i < 2; ++i)
            {
                DIR_LAST_COUNT[i] = bytes[pointer++];
            }

            for (int i = 0; i < 3; ++i)
            {
                CREATION_DATE[i] = bytes[pointer++];
            }
        }

        public DirectoryEntry Rename(string newName)
        {
            if (newName.Length > 12) newName = newName.Substring(0, 10) + newName.Substring(newName.Length - 1);

            if (!newName.EndsWith("\x3"))
            {
                newName += "\x3";
            }

            var counter = 0;
            foreach (var b in newName.ToCharArray().Select(x => (byte)x))
            {
                DIR_NAME[counter++] = b;
            }

            return this;
        }

        public string Name
        {
            get
            {
                var result = new string(DIR_NAME.Select(x => (char)x).ToArray()).Trim();

                for (int i = 0; i < result.Length; ++i)
                {
                    if (result.ToCharArray()[i] == (char)3)
                    {
                        result = result.Substring(0, i);
                        break;
                    }
                }

                return result;
            }
        }

        public byte[] ToArray()
        {
            var pointer = 0;
            var result = new byte[26];

            for (int i = 0; i < 12; ++i)
            {
                result[pointer++] = DIR_NAME[i];
            }

            result[pointer++] = (byte)DIR_ATTRIBUTES;

            for (int i = 0; i < 4; ++i)
            {
                result[pointer++] = DIR_START_BLOCK[i];
            }

            for (int i = 0; i < 2; ++i)
            {
                result[pointer++] = DIR_MAX_LENGTH[i];
            }

            for (int i = 0; i < 2; ++i)
            {
                result[pointer++] = DIR_USED_LENGTH[i];
            }

            for (int i = 0; i < 2; ++i)
            {
                result[pointer++] = DIR_LAST_COUNT[i];
            }

            for (int i = 0; i < 3; ++i)
            {
                result[pointer++] = CREATION_DATE[i];
            }

            return result;
        }

        public DateTime Date
        {
            get
            {
                //if (CREATION_DATE[1] > 11 || CREATION_DATE[2] > 31) return DateTime.MinValue;
                try
                {
                    return new DateTime(CREATION_DATE[0] + 1900, CREATION_DATE[1] + 1, CREATION_DATE[2] + 1);
                }
                catch (Exception ex)
                {
                    //Console.WriteLine(ex.ToString());
                }

                return DateTime.MinValue;

            }
        }

        public override string ToString()
        {
            if ((DIR_ATTRIBUTES & DirectoryAttributes.PermanentlyProtected) != DirectoryAttributes.PermanentlyProtected)
            {
                if (!string.IsNullOrEmpty(Name))
                {
                    return
                        $"{BitConverter.ToInt32(DIR_START_BLOCK, 0):00000} {BitConverter.ToUInt16(DIR_USED_LENGTH, 0):00000} {Name.Substring(Name.Length - 1)} {Name.Substring(0, Name.Length - 1),-11} {((DIR_ATTRIBUTES & DirectoryAttributes.FileDeleted) == DirectoryAttributes.FileDeleted ? "!" : " ")}  {Date:d}";
                }
                else
                {
                    return
                        $"{BitConverter.ToInt32(DIR_START_BLOCK, 0):00000} {BitConverter.ToUInt16(DIR_USED_LENGTH, 0):00000}   <UNNAMED>    {((DIR_ATTRIBUTES & DirectoryAttributes.FileDeleted) == DirectoryAttributes.FileDeleted ? "!" : " ")}  {Date:d}";
                }
            }
            return $"{BitConverter.ToInt32(DIR_START_BLOCK, 0):00000}   {BitConverter.ToUInt16(DIR_USED_LENGTH, 0):00000} {Name,-12} {((DIR_ATTRIBUTES & DirectoryAttributes.FileDeleted) == DirectoryAttributes.FileDeleted ? "!" : " ")}  {Date:d}";
        }
    }

    public class DirectoryBlock
    {
        public DirectoryHeader Header { get; set; }
        public DirectoryEntry[] Entries { get; } = new DirectoryEntry[38];

        public DirectoryBlock(byte[] data)
        {
            if (data.Length != 1024) throw new ArgumentException("Directory Blocks must be 1024 bytes long.");

            var temp = new byte[26];
            Array.ConstrainedCopy(data, 0, temp, 0, 26);
            Header = new DirectoryHeader(temp);

            for (int i = 26; i < 26 * 38; i += 26)
            {
                Array.ConstrainedCopy(data, i, temp, 0, 26);
                Entries[i / 26] = new DirectoryEntry(temp);
            }
        }

        public byte[] ToArray()
        {
            var bytes = new byte[1024];

            Array.ConstrainedCopy(Header.ToArray(), 0, bytes, 0, 26);

            for (int i = 1; i < 38; ++i)
            {
                if (Entries[i] != null)
                {
                    Array.ConstrainedCopy(Entries[i].ToArray(), 0, bytes, i * 26, 26);
                }
            }

            return bytes;
        }

        public DirectoryEntry Find(string filename)
        {
            var result = Entries.FirstOrDefault(x => x!=null && x.Name != null && x.Name.Trim() == filename);

            return result;
        }
    }

    public class DirectoryHeader
    {
        public byte[] VOL_NAME { get; } = new byte[12];
        public byte DIR_SIZE { get; set; } = 1;
        public byte[] DIR_CHECK { get; } = new byte[4];
        public byte[] VOL_SIZE { get; } = new byte[4];
        public byte[] RESERVED { get; } = new byte[2];
        public byte[] CREATION_DATE { get; } = new byte[3];

        public DirectoryHeader(string name, int blocks)
        {
            Rename(name);

            VOL_SIZE[0] = BitConverter.GetBytes(blocks)[0];
            VOL_SIZE[1] = BitConverter.GetBytes(blocks)[1];
            VOL_SIZE[2] = BitConverter.GetBytes(blocks)[2];
            VOL_SIZE[3] = BitConverter.GetBytes(blocks)[3];

            CREATION_DATE[0] = (byte)(DateTime.Now.Year - 1900);
            CREATION_DATE[1] = (byte)(DateTime.Now.Month - 1);
            CREATION_DATE[2] = (byte)(DateTime.Now.Day - 1);
        }

        public DirectoryHeader(byte[] bytes)
        {
            if (bytes.Length != 26) throw new ArgumentException("Invalid directory header data block.");

            var pointer = 0;
            for (int i = 0; i < 12; i++)
            {
                VOL_NAME[i] = bytes[pointer++];
            }

            DIR_SIZE = bytes[pointer++];

            for (int i = 0; i < 4; i++)
            {
                DIR_CHECK[i] = bytes[pointer++];
            }

            for (int i = 0; i < 4; i++)
            {
                VOL_SIZE[i] = bytes[pointer++];
            }

            for (int i = 0; i < 2; i++)
            {
                RESERVED[i] = bytes[pointer++];
            }

            for (int i = 0; i < 3; i++)
            {
                CREATION_DATE[i] = bytes[pointer++];
            }
        }

        public DirectoryHeader Rename(string newName)
        {
            if (newName.Length > 12) throw new ArgumentException("Name must be twelve characters or less.");

            var counter = 0;
            foreach (var b in newName.ToCharArray().Select(x => (byte)x))
            {
                VOL_NAME[counter++] = b;
            }

            return this;
        }

        public string Name
        {
            get
            {
                var result = new string(VOL_NAME.Select(x => (char)x).ToArray()).Trim();

                for (int i = 0; i < result.Length; ++i)
                {
                    if (result.ToCharArray()[i] == (char)3)
                    {
                        result = result.Substring(0, i);
                        break;
                    }
                }

                return result;
            }
        }
        public byte[] ToArray()
        {
            var result = new byte[26];

            var pointer = 0;

            for (int i = 0; i < 12; ++i) result[pointer++] = VOL_NAME[i];
            result[pointer++] = DIR_SIZE;
            for (int i = 0; i < 4; ++i) result[pointer++] = DIR_CHECK[i];
            for (int i = 0; i < 4; ++i) result[pointer++] = VOL_SIZE[i];
            for (int i = 0; i < 2; ++i) result[pointer++] = RESERVED[i];
            for (int i = 0; i < 3; ++i) result[pointer++] = CREATION_DATE[i];

            return result;
        }
    }

    [Flags]
    public enum DirectoryAttributes : byte
    {
        NotAFile = 1,
        ExecuteProtect = 2,
        FileDeleted = 4,
        SystemFile = 8,
        UserFile = 16,
        ReadProtected = 32,
        WriteProtected = 64,
        PermanentlyProtected = 128,
    }
}
